Skip to content

Conversation

@sunshowers
Copy link
Collaborator

@sunshowers sunshowers commented Jan 2, 2026

Previously, when a local OpenAPI document could be parsed—for example, because it contained conflict markers—the Dropshot API manager failed with a "couldn't be parsed" error. With this commit, the manager now gracefully handles the situation. This becomes particularly relevant after #38, after which conflicting versions are likely to be marked as rename/rename conflicts in Git.

This commit introduces unparseable file tracking, along with these problem and fix types:

  • Problem::UnparseableLocalFile for standalone unparseable files that need to be deleted.
  • Problem::BlessedVersionCorruptedLocal for unparseable files that match a blessed version's hash and can be regenerated.
  • Fix::RegenerateFromBlessed regenerates a corrupted file from blessed content, optionally as a git ref.
  • Fix::DeleteUnparseableFile removes orphaned unparseable files.

This commit is careful to only report unparseable files as problems if no other fix will overwrite them. (In those cases, regeneration will naturally clean up the corrupted file.)

For unparseable symlinks, we just treat them as missing.

Merge state awareness. During a git merge conflict, we now use MERGE_HEAD instead of HEAD when computing merge bases. This ensures blessed documents from the incoming branch are visible, which means that users won't have to do another generate run after finishing up resolution of merge conflicts.

Note that jj does not write out MERGE_HEAD, but that is okay because conflicts are first class in jj (in other words, it doesn't have modal/intermediate states like git). This means that the actual merge base of HEAD and main will be accurate.


This commit includes integration tests for:

  • Merge/rebase without conflicts.
  • Rename conflict resolution (git merge/rebase): when branches add different versions (v3 vs v4), git's rename detection causes conflicts that generate resolves.
  • Conflict with blessed versions, with git refs enabled (ie branches add the same version with different contents). In this situation, the user is expected to bump their own API change's version number. The API manager will then turn the blessed version into a git ref — ensure that this case works properly.

Also add the same set of tests for jj. Note that jj (currently) lacks rename detection during merges/rebases, and also that it turns conflicted symlinks into regular files. We test for both these conditions.

Created using spr 1.3.6-beta.1
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the Dropshot API manager to gracefully handle unparseable local OpenAPI files, such as those containing merge conflict markers from Git or Jujutsu. The tool now detects corrupted files, tracks them as problems, and provides automatic regeneration from blessed content.

Key changes:

  • Introduces tracking for unparseable files through a new LocalApiSpecFile::Unparseable variant and UnparseableFile struct
  • Adds new problem types (UnparseableLocalFile, BlessedVersionCorruptedLocal) and fix types (RegenerateFromBlessed, DeleteUnparseableFile) to handle corrupted files
  • Implements MERGE_HEAD detection in git merge base calculation to ensure blessed documents from incoming branches are visible during merge conflicts
  • Adds comprehensive integration tests covering merge and rebase scenarios for both git and jj, including conflict resolution workflows

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/integration-tests/tests/integration/git_ref.rs Adds comprehensive integration tests for merge/rebase scenarios with git and jj, testing conflict resolution when git refs are involved
crates/integration-tests/src/lib.rs Exports new test result types and helper functions for merge/rebase testing
crates/integration-tests/src/fixtures.rs Adds new API fixtures (v1_v2_v4, v3_alternate, v1_v2_v3_v4alt) for testing merge conflict scenarios
crates/integration-tests/src/environment.rs Adds merge/rebase result enums and helper methods for git and jj operations, including conflict detection
crates/dropshot-api-manager/src/spec_files_local.rs Converts LocalApiSpecFile to an enum with Valid and Unparseable variants to track corrupted files
crates/dropshot-api-manager/src/spec_files_generic.rs Adds UnparseableFile struct, SpecFileInfo trait, and updates ApiLoad trait to support unparseable files
crates/dropshot-api-manager/src/spec_files_generated.rs Implements new ApiLoad trait methods for generated files (unparseable files not allowed)
crates/dropshot-api-manager/src/spec_files_blessed.rs Implements new ApiLoad trait methods for blessed files (unparseable files not allowed)
crates/dropshot-api-manager/src/resolved.rs Adds logic to detect, track, and regenerate corrupted local files; includes new Problem and Fix variants
crates/dropshot-api-manager/src/git.rs Updates git_merge_base_head to use MERGE_HEAD during active merges for correct blessed document resolution
crates/dropshot-api-manager/src/cmd/debug.rs Updates debug output to handle unparseable files gracefully
.github/workflows/ci.yml Adds jj-cli installation for running jj integration tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Created using spr 1.3.6-beta.1
@sunshowers
Copy link
Collaborator Author

Manual test notes

Without git ref storage enabled

Both branches add v3 but with different implementations.

  • branch_a: v3 with Number wrapper type (hash: b48dc9)
  • branch_b: v3 with /status endpoint (hash: e47ffc)

Setup

# Create isolated test environment.
rm -rf /tmp/merge-test && mkdir /tmp/merge-test
cp -r /home/rain/dev/oxide/dropshot-api-manager /tmp/merge-test/workspace
cd /tmp/merge-test/workspace
rm -rf .git .jj target
git init && git add -A && git commit -m "initial"

# Create baseline (v1, v2 only)—edit lib.rs to remove v3, revert ThingV2 to u32.
cargo example-openapi generate --blessed-from-dir e2e-example/documents
git add -A && git commit -m "baseline: v1, v2 only"

# Create branch_a: v3 with Number wrapper.
git checkout -b branch_a
# Edit lib.rs: add v3, change ThingV2 to use Number wrapper.
cargo example-openapi generate --blessed-from-git main
git add -A && git commit -m "add v3 (implementation A: Number wrapper)"
# Produces: versioned-3.0.0-b48dc9.json

# Create branch_b: v3 with /status endpoint.
git checkout main && git checkout -b branch_b
# Edit lib.rs: add v3, add get_status endpoint and StatusResponse.
cargo example-openapi generate --blessed-from-git main
git add -A && git commit -m "add v3 (implementation B: /status endpoint)"
# Produces: versioned-3.0.0-e47ffc.json (different hash)

Git merge test

git merge branch_a
# Conflicts: lib.rs and versioned-latest.json.
# Both versioned-3.0.0-b48dc9.json and versioned-3.0.0-e47ffc.json exist.

# Manually resolve lib.rs: merge both implementations (A's v3 stays, B's becomes v4).

cargo example-openapi check --blessed-from-git main

Check output before generate:

       Stale versioned (versioned v3.0.0 (added locally)): Versioned API
         problem: Extra (incorrect) OpenAPI documents were found for locally-
                  added version: versioned/versioned-3.0.0-e47ffc.json.
             fix: will delete file: versioned/versioned-3.0.0-e47ffc.json
       Stale versioned (versioned v4.0.0 (added locally)): Versioned API
         problem: No OpenAPI document was found for this locally-added API version.
             fix: will write new file versioned/versioned-4.0.0-b6f509.json
       Stale versioned "latest" symlink
         problem: points to versioned-3.0.0-e47ffc.json, but should be versioned-4.0.0-b6f509.json
       Stale 5 documents checked: 3 fresh, 2 stale, 0 failed, 1 other problem
cargo example-openapi generate --blessed-from-git main

Generate output:

       Fixed removed versioned-3.0.0-e47ffc.json
       Fixed created versioned-4.0.0-b6f509.json: Updated
       Fixed wrote link versioned-latest.json -> versioned-4.0.0-b6f509.json
     Success 5 documents: 3 changes made, 3 unchanged, 0 failed
cargo example-openapi check --blessed-from-git main

Check output after generate:

       Fresh versioned (versioned v3.0.0 (added locally)): Versioned API
       Fresh versioned (versioned v4.0.0 (added locally)): Versioned API
       Fresh versioned "latest" symlink
     Success 5 documents checked: 6 fresh, 0 stale, 0 failed, 0 other problems
git add -A && git commit -m "merge: A's v3 + B's v3 becomes v4"

jj merge test

# Reset and init jj.
git checkout main && git branch -D branch_a branch_b
git reset --hard <baseline-commit>
jj git init --colocate

# Create branch_a with v3 (implementation A).
# Edit lib.rs: add v3 with Number wrapper.
cargo example-openapi generate --blessed-from-git main
jj commit -m "add v3 (implementation A)" && jj bookmark create branch_a -r @-

# Create branch_b with v3 (implementation B).
jj new main -m "add v3 (implementation B)"
# Edit lib.rs: add v3 with /status endpoint.
cargo example-openapi generate --blessed-from-git main
jj bookmark create branch_b

# Merge.
jj new branch_a branch_b -m "merge"
# Reports conflict on lib.rs and symlink.
cat e2e-example/documents/versioned/versioned-latest.json
# jj converted symlink to regular file with conflict markers.
# Manually resolve lib.rs.
cargo example-openapi check --blessed-from-git main

Check output before generate (jj):

     Warning expected symlink but found regular file "versioned-latest.json"
       Stale versioned (versioned v3.0.0 (added locally)): Versioned API
         problem: Extra (incorrect) OpenAPI documents were found for locally-
                  added version: versioned/versioned-3.0.0-e47ffc.json.
             fix: will delete file: versioned/versioned-3.0.0-e47ffc.json
       Stale versioned (versioned v4.0.0 (added locally)): Versioned API
         problem: No OpenAPI document was found for this locally-added API version.
             fix: will write new file versioned/versioned-4.0.0-b6f509.json
       Stale versioned "latest" symlink
         problem: points to versioned-3.0.0-e47ffc.json, but should be versioned-4.0.0-b6f509.json
       Stale 5 documents checked: 3 fresh, 2 stale, 0 failed, 1 other problem
cargo example-openapi generate --blessed-from-git main

Generate output (jj):

     Warning expected symlink but found regular file "versioned-latest.json"; will regenerate
       Fixed removed versioned-3.0.0-e47ffc.json
       Fixed created versioned-4.0.0-b6f509.json: Updated
       Fixed wrote link versioned-latest.json -> versioned-4.0.0-b6f509.json
     Success 5 documents: 3 changes made, 3 unchanged, 0 failed
cargo example-openapi check --blessed-from-git main
# Success: 5 documents checked, 6 fresh, 0 stale, 0 failed, 0 other problems.

jj status  # No conflict marker.

With git ref storage enabled

Setup

# Create isolated test environment.
rm -rf /tmp/merge-test && mkdir /tmp/merge-test
cp -r /home/rain/dev/oxide/dropshot-api-manager /tmp/merge-test/workspace
cd /tmp/merge-test/workspace
rm -rf .git .jj target
git init && git add -A && git commit -m "initial"

# Enable git ref storage in e2e-example/bin/src/main.rs.
# Uncomment .with_git_ref_storage().

# Create baseline with v1, v2, v3.
cargo example-openapi generate --blessed-from-dir e2e-example/documents
git add -A && git commit -m "enable git ref storage; baseline v1, v2, v3"

# Add v4 to main (implementation A: /status endpoint).
# Edit lib.rs: add v4 with StatusResponse and /status endpoint.
cargo example-openapi generate --blessed-from-git main
git add -A && git commit -m "add v4 (implementation A: /status endpoint)"
# v1, v2, v3 converted to .gitref files; v4 is JSON.

# Create branch_b from baseline (before v4), add different v4.
git checkout -b branch_b <baseline-commit>
# Edit lib.rs: add v4 with Metadata and /metadata endpoint.
cargo example-openapi generate --blessed-from-git <baseline-commit>
git add -A && git commit -m "add v4 (implementation B: /metadata endpoint)"
# v1, v2, v3 converted to .gitref files; v4 is JSON (different hash).

Git merge test

git merge main
# Reports:
#   conflict (content): e2e-example/apis/src/lib.rs
#   conflict (rename/rename): versioned-3.0.0-b48dc9.json renamed to
#     versioned-4.0.0-f05d45.json in HEAD and to versioned-4.0.0-dc7291.json in main
#   conflict (content): versioned-latest.json

Git detects a rename/rename conflict because both branches renamed
versioned-3.0.0-b48dc9.json to different v4 files. The .gitref files merged
cleanly, since they have the same contents.

# After manually resolving lib.rs: A's v4 stays as v4, B's becomes v5.
cargo example-openapi check --blessed-from-git main

Check output before generate:

     Warning skipping unparseable file: "versioned-4.0.0-f05d45.json"
     Warning skipping unparseable file: "versioned-4.0.0-dc7291.json"
       Fresh versioned (versioned v1.0.0 (blessed)): Versioned API
       Fresh versioned (versioned v2.0.0 (blessed)): Versioned API
       Fresh versioned (versioned v3.0.0 (blessed)): Versioned API
       Stale versioned (versioned v4.0.0 (blessed)): Versioned API
         problem: Local file for this blessed version is corrupted
             fix: will regenerate versioned/versioned-4.0.0-dc7291.json from blessed content as git ref
         problem: Extra OpenAPI document: versioned/versioned-4.0.0-f05d45.json
             fix: will delete file
       Stale versioned (versioned v5.0.0 (added locally)): Versioned API
         problem: No OpenAPI document was found
             fix: will write new file versioned/versioned-5.0.0-cedf2c.json
       Stale versioned "latest" symlink
       Stale 6 documents checked: 4 fresh, 2 stale, 0 failed, 1 other problem
cargo example-openapi generate --blessed-from-git main

Generate output:

       Fixed removed corrupted file versioned-4.0.0-dc7291.json
       Fixed created git ref versioned-4.0.0-dc7291.json.gitref
       Fixed removed versioned-4.0.0-f05d45.json
       Fixed created versioned-5.0.0-cedf2c.json: Updated
       Fixed wrote link versioned-latest.json -> versioned-5.0.0-cedf2c.json
     Success 6 documents: 4 changes made, 4 unchanged, 0 failed
cargo example-openapi check --blessed-from-git main
# Success: 6 documents checked, 7 fresh, 0 stale, 0 failed, 0 other problems.
git add -A && git commit -m "merge: main's v4 + branch_b's v4 becomes v5"

jj merge test

# Reset and init jj.
git reset --hard <baseline-commit>
jj git init --colocate

# Add v4 on main (implementation A).
# Edit lib.rs: add v4 with /status endpoint.
cargo example-openapi generate --blessed-from-git main
jj commit -m "add v4 (implementation A)" && jj bookmark set main -r @-

# Create branch_b from baseline with v4 (implementation B).
jj new <baseline> -m "add v4 (implementation B)"
# Edit lib.rs: add v4 with /metadata endpoint.
cargo example-openapi generate --blessed-from-git <baseline>
jj bookmark create branch_b

# Merge.
jj new main branch_b -m "merge"
# Reports:
#   e2e-example/apis/src/lib.rs         2-sided conflict
#   versioned-latest.json               2-sided conflict including a symlink

jj does not report rename/rename conflicts, since it currently lacks merge-time
rename detection. This means that only lib.rs and symlink conflicts are
detected. Both v4 JSON files exist.

cat e2e-example/documents/versioned/versioned-latest.json
# jj converted symlink to regular file with conflict markers.
# Manually resolve lib.rs: A's v4 stays as v4, B's becomes v5.
cargo example-openapi check --blessed-from-git main

Check output before generate:

     Warning expected symlink but found regular file "versioned-latest.json"; will regenerate
       Fresh versioned (versioned v1.0.0 (blessed)): Versioned API
       Fresh versioned (versioned v2.0.0 (blessed)): Versioned API
       Fresh versioned (versioned v3.0.0 (blessed)): Versioned API
       Stale versioned (versioned v4.0.0 (blessed)): Versioned API
         problem: Blessed non-latest version is stored as a full JSON file
             fix: will convert versioned/versioned-4.0.0-dc7291.json to git ref
         problem: Extra OpenAPI document: versioned/versioned-4.0.0-f05d45.json
             fix: will delete file
       Stale versioned (versioned v5.0.0 (added locally)): Versioned API
         problem: No OpenAPI document was found
             fix: will write new file versioned/versioned-5.0.0-cedf2c.json
       Stale versioned "latest" symlink
         problem: "Latest" symlink is missing
       Stale 6 documents checked: 4 fresh, 2 stale, 0 failed, 1 other problem
cargo example-openapi generate --blessed-from-git main

Generate output:

     Warning expected symlink but found regular file "versioned-latest.json"; will regenerate
       Fixed converted versioned-4.0.0-dc7291.json to git ref
       Fixed created versioned-4.0.0-dc7291.json.gitref
       Fixed removed versioned-4.0.0-f05d45.json
       Fixed created versioned-5.0.0-cedf2c.json: Updated
       Fixed wrote link versioned-latest.json -> versioned-5.0.0-cedf2c.json
     Success 6 documents: 4 changes made, 4 unchanged, 0 failed
cargo example-openapi check --blessed-from-git main
# Success: 6 documents checked, 7 fresh, 0 stale, 0 failed, 0 other problems.

jj status  # No conflict marker.

Created using spr 1.3.6-beta.1
@sunshowers sunshowers changed the title [2/n] handle unparseable local files (e.g., merge conflict markers) [2/n] handle unparseable local files (e.g., conflict markers) Jan 2, 2026
Created using spr 1.3.6-beta.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants